<script src="//unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<template>
  <div ref="list" class="list-view" @scroll="handleScroll($event)">
    <div class="list-view-phantom" :style="{ height: data.length * 30 + 'px' }"></div>
    <div ref="content" class="list-view-content">
      <div class="list-view-item" v-for="item in visibleData">{{ item.value }}</div>
    </div>
  </div>
</template>
</div>

<script>
new Vue({
  props: {
    itemHeight: {
      type: Number,
      default: 30
    }
  },

  mounted() {
    this.visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
    this.start = 0;
    this.end = this.start + this.visibleCount;
    this.visibleData = this.data.slice(this.start, this.end);
  },

  data() {
    const data = [];
    for (let i = 0; i < 10000; i++) {
      data.push({ value: i });
    }

    return {
      data,
      start: 0,
      end: null,
      visibleCount: null,
      visibleData: [],
      scrollTop: 0
    };
  },

  methods: {
    handleScroll() {
      const scrollTop = this.$refs.list.scrollTop;
      const fixedScrollTop = scrollTop - scrollTop % 30;
      this.$refs.content.style.webkitTransform = `translate3d(0, ${fixedScrollTop}px, 0)`;

      this.start = Math.floor(scrollTop / 30);
      this.end = this.start + this.visibleCount;
      this.visibleData = this.data.slice(this.start, this.end);
    }
  }    
}).$mount('#app')
</script>

.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  border: 1px solid #666;
}

.list-view-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}

.list-view-item {
  padding: 5px;
  color: #666;
  height: 30px;
  line-height: 30px;
  box-sizing: border-box;
}